﻿/**	URLVars:  This is an alternative to URLVariables.
*	FEATURES:
*	- This class has a robust URL Decoder that handles most all strings in
*	encounters.
*	- It properly evaluates dot notation to create iteratively deepening
*	Object trees.
*	- It can also automatically convert all values to thier proper types.
*	- In addition to creating entire object trees for your data
*	you can choose to aquire the data in the form of an array of name/value
*	pairs.
*	- Its processing is on the order of O(n).
*	- Its in the public domain.
*	- If you use it long enough it just might give you pie.
*
*	@author		David Wipperfurth
*	@editor		none
*	@group		Blue Solutions
*	@date		9/16/08
*	@update		9/17/08
*	@index		none
*	@version	1.0
*/

package unitescore.utils {
	
	import flash.system.System;
	
	public dynamic class URLVars{
		
		/**	Strip Ands:  Takes in a URL Encoded String and removes all extra
		*	& to the point that URLVariables will accept it.  It does NOT
		*	remove junk in between &s, so this does not garauntee URLVariables
		*	will accept the string, but it certainly help. :)
		*
		*	@param	String	The URL Variable string to strip extra &s from.
		*	@return String	Condenced URL Variable String.
		*/
		static public function stripAnds(urlEncoding:String):String{
			var x=-1;
			while((x = urlEncoding.indexOf("&", x+1))!= -1){
				var y=x;
				while(urlEncoding.charAt(++y)=="&");
				urlEncoding = urlEncoding.substring(0,x+1)+urlEncoding.substring(y);
			}
			if(urlEncoding.charAt(0)=="&")urlEncoding = urlEncoding.substring(1);
			if(urlEncoding.charAt(urlEncoding.length-1)=="&")urlEncoding = urlEncoding.substring(0,urlEncoding.length-1);
			return urlEncoding;
		}
		
		/**	Percent Decode:  Turns things like '%21' into '!'.  To prevent
		*	odd outcomes it perposfully does not accurately resolve ascii chars
		*	in the range 0-31 or 127.  This function is much the same as the
		*	built-in AS3 function 'decodeURIComponent' although it converts
		*	a larger range of characters and is less likely to complain.
		*
		*	@param	String	String with Percent Encoding e.g. "Hello%20World"
		*	@return	String	Decoded Percent String e.g. "Hello World"
		*/
		static public function percentDecode(urlEncoding:String):String{
			var startIndex:int = urlEncoding.indexOf("%", 0);
			while(startIndex!= -1){
				
				var charNum = int("0x"+urlEncoding.substr(startIndex+1, 2));
				var char:String; 
				if(charNum < 32 || charNum == 127) char = "";		//prevents odd behavior
				else char = String.fromCharCode(charNum);
				
				urlEncoding = urlEncoding.substring(0,startIndex)	//before encoded
				+ char												//decoding 
				+ urlEncoding.substring(startIndex+3);				//after encoded
				
				startIndex = urlEncoding.indexOf("%", startIndex+1);
			}
			return urlEncoding;
		}
		
		public const maxBuffLen:int = 64;		//How many characters can be pulled for name
		public const maxObjectDepth:int = 32;	//How deep an object tree can be
		
		protected var urlEncoding:String;		//The string to convert
		
		private var urlIndex:int = 0;			//A pointer a location in the urlEncoding string
		private var pairs:Array;				//holds name/values pairs
		
		/**	Constructor:  The only function you need to know.  Send a URL
		*	Variable String into this function and your returned URLVars
		*	Object is the root of the object tree generated by the variables
		*	in the URL Encoding.
		*
		*	@param	String	The URL Variable String to decode.
		*	@param	Boolean	True if you want URLVars to automatically fix types
		*			to be things other than strings if possible.
		*	@param	Boolean True if you want Percent Encoding the get Decoded
		*			(This decoding is done BEFORE type conversion)
		*	@param	Boolean	True if you would like this URLVars to store the
		*			results in an array that can be accessed by toString() and
		*			by contence().
		*/
		public function URLVars(urlEncoded:String, evalTypes:Boolean = true,
								perDecode:Boolean = true, keepArray = false){
			urlEncoding = urlEncoded;
			pairs = new Array(0);
			urlIndex = 0;
			parsePairs();
			if(perDecode)percentDecodeValues();
			if(evalTypes)evalValueTypes();
			convertPairsToVariables();
			if(!keepArray) pairs = null;
			//System.gc();
		}
		
		/**	To String:  Standard Over-ride of Object.toString().  This
		*	will return a comma seperated name/value pair list if keepArray
		*	is set to true, and will return the original URL Variable String
		*	if keepArray is false.
		*
		*	@return String	String representation of URLVars Object
		*/
		public function toString(){
			if(pairs == null) return urlEncoding;
			return pairs.toString(); 
		}
		
		/**	To URL String:  Like toString(), but always returns the URL Variable
		*	String.
		*
		*	@return String	String representation of URLVars Object
		*/
		public function toURLString(){ return urlEncoding; }
		
		/** Contence:  returns an array of name/value pairs stored in the
		*	URLVars object.  This functino only works if keepArray is true
		*	otherwise it returns null.
		*
		*	@return	Array Array representation of URLVars Object
		*/
		public function contence():Array{ return pairs; }
		
		/**	Convert Pairs To Variables:  <-- Does That.
		*/
		protected function convertPairsToVariables(){
			for(var i=0;i<pairs.length;i++){
				setRef(pairs[i][0], pairs[i][1]);
			}
		}
		
		/**	Set Reference:  Evaluates a path/name written with dot notation
		*	and stores the value in it.
		*	Ex:  setRef("my.big.long.path", "is fun");
		*	     trace(my.big.long.path)	//output: is fun
		*
		*	@param	String	The path/name to convert to a reference
		*	@param	*	A value to store in the reference that is created
		*	@return	The value arguement served-up from the arguement reference
		*/
		protected function setRef(namePath:String, value:*):Object{
			var ref = this;
			var nodes:Array = namePath.split(".", maxObjectDepth);
			for(var i=0;i<nodes.length-1;i++){
				if(ref[nodes[i]]==null) ref[nodes[i]] = new Object();
				ref = ref[nodes[i]];
			}
			if(ref[nodes[nodes.length-1]]==null) ref[nodes[nodes.length-1]] = value;
			return ref;
		}
		
		/**	Evaluate Types:  Changes string representations of other types into
		*	those other types. Currently Converts to:
		*	- int
		*	- Number (if the string value is not an integer)
		*	- Object (if the string is just the word 'object')
		*	- Boolean
		*
		*/
		protected function evalValueTypes(){
			for(var i=0;i<pairs.length;i++){
				if(pairs[i][1].search(/[^0-9\.\-]/)==-1){	//if string leave as string
					//choose int or number
					var val:Number = Number(pairs[i][1]);
					if(val == Math.floor(Math.abs(val))) pairs[i][1] = int(val);
					else pairs[i][1] = val;
				}else{
					//choose Boolean, Object, or String
					switch(pairs[i][1].toLowerCase()){
						case "object":	pairs[i][1] = new Object();	break;
						case "true":	pairs[i][1] = true;			break;
						case "false":	pairs[i][1] = false;		break;
						default:  break;	//leave as string
					}
				}
			}
		}
		
		/**	Percent Decode Values:  Runs the static decoder on each VALUE in the
		*	name/vale pairs.  Names do not ever get percent decoding.
		*/
		protected function percentDecodeValues(){
			for(var i=0;i<pairs.length;i++){
				pairs[i][1] = percentDecode(pairs[i][1]);
			}
		}
		
		/**	Parse Pairs:  finds name/value pairs in the URI String
		*	and stores them in an array.
		*/
		protected function parsePairs(){
			var newPair:Array = grabPair();
			while(newPair != null){
				pairs.push(newPair);
				newPair = grabPair();
			}
		}
		
		/** Grab Pairs:  finds a single name/value pair in the URI String 
		*	and stores it in an 2 slot array.
		*
		*	@return	Array	The name/pair couple
		*/
		protected function grabPair():Array{
			var name:String = grabName();
			while(name == ""){
				if(urlIndex >= urlEncoding.length) return null;
				name = grabName();
			}

			var value:String = grabValue();
			if(value=="" && urlIndex >= urlEncoding.length) return null;
			
			return new Array(name, value);
		}
		
		/**	Grab Name:  finds and returns the next name in the URI string
		*
		*	@return	String	The next name found
		*/
		protected function grabName():String{
			var endIndex:int = urlIndex;
			var buffLen:int = 0;
			while(urlEncoding.charAt(endIndex)!= "="){
				if(buffLen > maxBuffLen){
					while(urlEncoding.charAt(endIndex) != "&") endIndex++;
					urlIndex = endIndex;
					return "";
				}
				if(urlEncoding.charAt(endIndex) == "&" 
				|| urlEncoding.charAt(endIndex) == ""){
					urlIndex = endIndex + 1;
					return "";
				}
				buffLen++;
				endIndex++;
			}
			var name = urlEncoding.substring(urlIndex, endIndex);
			urlIndex = endIndex+1;
			return name;
		}
		
		/**	Grab Value:  finds and returns the next value in the URI string
		*
		*	@return	String	The next value found
		*/
		protected function grabValue():String{
			var endIndex:int = urlIndex;
			while(urlEncoding.charAt(endIndex)!= "&"
			&& urlEncoding.charAt(endIndex) != ""){
				endIndex++;
			}
			var value = urlEncoding.substring(urlIndex, endIndex);
			urlIndex = endIndex+1;
			return value;
		}
		
	}
	
}